﻿using System;
using System.Collections.Generic;
using UnityEngine;

namespace CWM.Skinn
{
    public static partial class SkinnEx
    {
        public static MeshRenderer CombinedStaticMeshes(this Renderer[] renderers, bool mergeMaterials)
        {
            var tempRenderer = new List<Renderer>();
            for (int i = 0; i < renderers.Length; i++)
            {
                if (IsNullOrNotVailid(renderers[i])) continue;
                if (!(renderers[i] as MeshRenderer)) continue;

                var tempSubmeshes = MeshRendererSplit((MeshRenderer)renderers[i]);
                foreach (var item in tempSubmeshes)
                {
                    var mesh = item.GetSharedMesh();
                    Vector3[] vertices;
                    Vector3[] normals;
                    if (mesh.vertices.Transformed(out vertices, item.transform.localToWorldMatrix, false)) mesh.vertices = vertices;
                    if (mesh.normals.Transformed(out normals, item.transform.localToWorldMatrix, true)) mesh.normals = normals;
                    if (!IsNullOrEmpty(mesh.tangents)) mesh.RecalculateTangents();
                    item.transform.localPosition = Vector3.zero;
                    item.transform.localRotation = Quaternion.identity;
                    item.transform.localScale = Vector3.one;
                }
                tempRenderer.AddRange(tempSubmeshes);
            }
            if (tempRenderer.Count < 1) return null;
            var combinedRenderer = CombineRenderers(tempRenderer.ToArray(), false, mergeMaterials);
            foreach (var temp in tempRenderer) { Release(temp.GetSharedMesh()); Release(temp.gameObject); }
            return (MeshRenderer)combinedRenderer;
        }

        public static SkinnedMeshRenderer CombinedMeshes(this Renderer[] renderers, bool mergeMaterials)
        {
            var tempRenderer = new List<Renderer>();
            for (int i = 0; i < renderers.Length; i++)
            {
                if (!HasMinimumRequirements(renderers[i] as SkinnedMeshRenderer, true)) continue;
                var tempSubmeshes = SkinnedMeshSplit((SkinnedMeshRenderer)renderers[i]);
                tempRenderer.AddRange(tempSubmeshes);
            }
            if (tempRenderer.Count < 1) return null;
            var combinedRenderer = CombineRenderers(tempRenderer.ToArray(), true, mergeMaterials);
            foreach (var temp in tempRenderer) { Release(temp.GetSharedMesh()); Release(temp.gameObject); }
            return (SkinnedMeshRenderer)combinedRenderer;
        }

        private static Renderer CombineRenderers(Renderer[] renderers, bool skinned, bool mergeMaterials)
        {
            var vertexInfo = new MeshVertexInfo();
            var finalMaterials = new List<Material>();
            var combinedRenderers = new Dictionary<int, List<Renderer>>();

            var boneWeights = (skinned) ? new List<BoneWeight>(vertexInfo.vertexCount) : null;
            var boneNames = new List<int>();
            var bones = new List<Transform>();
            var bindposes = new List<Matrix4x4>();
            int vertexCount = 0;

            for (int i = 0; i < renderers.Length; i++)
            {
                var mat = renderers[i].sharedMaterial; if (!mat) mat = SkinnGizmos.GizmoMaterial;
                if (mergeMaterials)
                {
                    int matId = mat.GetInstanceID();
                    if (combinedRenderers.ContainsKey(matId))
                    {
                        var mats = new List<Renderer>();
                        combinedRenderers.TryGetValue(matId, out mats);
                        combinedRenderers.Remove(matId);
                        mats.Add(renderers[i]);
                        combinedRenderers.Add(matId, mats);
                    }
                    else{ combinedRenderers.Add(matId, new List<Renderer>(1) { renderers[i] }); finalMaterials.Add(mat); }
                }
                else { combinedRenderers.Add(i, new List<Renderer>(1) { renderers[i] }); finalMaterials.Add(mat); }

                var sharedMesh = renderers[i].GetSharedMesh();
                vertexInfo.Add(sharedMesh);

                if (!skinned) continue;

                var smrBones = (renderers[i] as SkinnedMeshRenderer).bones;
                var smrBoneNames = smrBones.GetNamesToHashes();
                var smrBindposes = sharedMesh.bindposes;

                for (int ii = 0; ii < smrBones.Length; ii++)
                {
                    if (boneNames.Contains(smrBoneNames[ii])) continue;
                    bones.Add(smrBones[ii]); boneNames.Add(smrBoneNames[ii]); bindposes.Add(smrBindposes[ii]);
                }
            }

            var indexIndices = new Dictionary<Renderer, Vector2Int[]>(renderers.Length);
            var triangleIndices = new Dictionary<int, int[]>(vertexInfo.vertexCount);
            var subMeshCount = 0;
            var combinedData = new MeshVertexDataCombined(vertexInfo);

            foreach (var item in combinedRenderers)
            {
                var subRenderers = item.Value;
                var subMeshVertexInfo = new MeshVertexInfo();
                for (int i = 0; i < subRenderers.Count; i++) { subMeshVertexInfo.Add(subRenderers[i]); }
                var triangles = new List<int>(subMeshVertexInfo.triangleCount);
                for (int i = 0; i < subRenderers.Count; i++)
                {
                    var subRenderer = subRenderers[i];
                    var sharedMesh = subRenderer.GetSharedMesh();
                    var meshData = new MeshVertexData(sharedMesh);
                    var orignalIndex = new List<Vector2Int>();
                    var tempIndices = new Dictionary<int, int>();
                    var transforms = skinned ? (subRenderer as SkinnedMeshRenderer).bones : null;
                    var transformHashes = skinned ? transforms.GetNamesToHashes() : new int[0];

                    var subWeights = skinned ? meshData.boneWeights : null;
                    var indices = sharedMesh.triangles;
                    for (int ii = 0; ii < indices.Length; ii++)
                    {
                        int currentIndex = indices[ii];
                        int newIndex;
                        if (!tempIndices.TryGetValue(currentIndex, out newIndex))
                        {
                            newIndex = combinedData.AddVertexData(meshData, currentIndex);
                            if (skinned)
                            {
                                BoneWeight boneWeight = new BoneWeight
                                {
                                    boneIndex0 = subWeights[currentIndex].boneIndex0,
                                    boneIndex1 = subWeights[currentIndex].boneIndex1,
                                    boneIndex2 = subWeights[currentIndex].boneIndex2,
                                    boneIndex3 = subWeights[currentIndex].boneIndex3,
                                    weight0 = subWeights[currentIndex].weight0,
                                    weight1 = subWeights[currentIndex].weight1,
                                    weight2 = subWeights[currentIndex].weight2,
                                    weight3 = subWeights[currentIndex].weight3
                                };
                                if (boneWeight.weight0 > 0) boneWeight.boneIndex0 = boneNames.GetIndex(transformHashes[boneWeight.boneIndex0], true);
                                if (boneWeight.weight1 > 0) boneWeight.boneIndex1 = boneNames.GetIndex(transformHashes[boneWeight.boneIndex1], true);
                                if (boneWeight.weight2 > 0) boneWeight.boneIndex2 = boneNames.GetIndex(transformHashes[boneWeight.boneIndex2], true);
                                if (boneWeight.weight3 > 0) boneWeight.boneIndex3 = boneNames.GetIndex(transformHashes[boneWeight.boneIndex3], true);
                                boneWeights.Add(boneWeight);
                            }
                            tempIndices.Add(currentIndex, newIndex);
                            orignalIndex.Add(new Vector2Int(vertexCount, currentIndex));
                            vertexCount++;
                        }
                        triangles.Add(newIndex);
                    }
                    indexIndices.Add(subRenderer, orignalIndex.ToArray());
                }

                triangleIndices.Add(subMeshCount, triangles.ToArray());
                subMeshCount++;
            }

            var mesh = combinedData.GetMesh(triangleIndices);
            mesh.CopyBlendshapes(indexIndices);

            if (!skinned)
            {
                var renderer = new GameObject("CombinedStaicMesh", new Type[] { typeof(MeshFilter) }).AddComponent<MeshRenderer>();
                renderer.SetSharedMesh(mesh);
                renderer.sharedMaterials = finalMaterials.ToArray();
                return renderer;
            }
            else
            {
                mesh.boneWeights = boneWeights.ToArray();
                mesh.bindposes = bindposes.ToArray();

                var renderer = new GameObject("CombinedSkinnedMesh").AddComponent<SkinnedMeshRenderer>();
                renderer.SetSharedMesh(mesh);
                renderer.bones = bones.ToArray();
                renderer.sharedMaterials = finalMaterials.ToArray();
                renderer.rootBone = renderer.bones.FindMinHierarchicalOrder();
                renderer.SetBounds();

                return renderer;
            }
        }
    }
}